#!/usr/bin/env python
# -*- coding: UTF-8 -*-
'''
@Project ：V2 
@File    ：GasSchedulingGurobi.py
@IDE     ：PyCharm 
@Author  ：郭星
@Date    ：2025/10/22 22:15 
'''
import gurobipy as gp
from gurobipy import GRB
import json
import os
from typing import Dict, List


class GasSchedulingGurobi:
    def __init__(self, instance_path: str):
        self.instance = self.load_instance(instance_path)
        self.params = self.instance['parameters']
        self.processing_times = self.instance['processing_times']
        self.model = gp.Model("GasScheduling")

        # 初始化参数
        self.num_workpieces = self.params['num_workpieces']
        self.num_components = self.params['num_components']
        self.num_clean_eq = self.params['num_clean_eq']
        self.num_inflation_eq = self.params['num_inflation_eq']
        self.num_shake_eq = self.params['num_shake_eq']
        self.num_analysis_eq_per_comp = self.params['num_analysis_eq_per_component']

        # 处理时间
        self.clean_time = self.processing_times['clean_time']
        self.inflation_time = self.processing_times['inflation_time']
        self.shake_time = [self.num_components * self.processing_times['shake_unit_time']
                           for _ in range(self.num_workpieces)]
        self.analysis_time = self.processing_times['analysis_time']

        # 决策变量
        self.vars = self._create_variables()
        # 约束
        self._add_constraints()
        # 目标函数
        self._set_objective()

    def load_instance(self, file_path: str) -> Dict:
        """加载算例文件"""
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"算例文件不存在: {file_path}")

        with open(file_path, 'r', encoding='utf-8') as f:
            instance = json.load(f)

        # 兼容一维充气时间格式
        if len(instance['processing_times']['inflation_time']) == self.params['num_workpieces']:
            instance['processing_times']['inflation_time'] = [
                [t for _ in range(self.params['num_components'])]
                for t in instance['processing_times']['inflation_time']
            ]
        return instance

    def _create_variables(self) -> Dict:
        """创建决策变量"""
        vars_dict = {}

        # 清洗阶段: 每个工件在清洗设备上的开始/结束时间
        vars_dict['clean_start'] = self.model.addVars(
            self.num_workpieces, self.num_clean_eq,
            vtype=GRB.CONTINUOUS, name="clean_start"
        )
        vars_dict['clean_end'] = self.model.addVars(
            self.num_workpieces, self.num_clean_eq,
            vtype=GRB.CONTINUOUS, name="clean_end"
        )
        # 清洗设备分配指示变量
        vars_dict['clean_assign'] = self.model.addVars(
            self.num_workpieces, self.num_clean_eq,
            vtype=GRB.BINARY, name="clean_assign"
        )

        # 充装阶段: 每个工件的每个组分在充装设备上的开始/结束时间
        vars_dict['inflate_start'] = self.model.addVars(
            self.num_workpieces, self.num_components, self.num_inflation_eq,
            vtype=GRB.CONTINUOUS, name="inflate_start"
        )
        vars_dict['inflate_end'] = self.model.addVars(
            self.num_workpieces, self.num_components, self.num_inflation_eq,
            vtype=GRB.CONTINUOUS, name="inflate_end"
        )
        # 充装设备分配指示变量
        vars_dict['inflate_assign'] = self.model.addVars(
            self.num_workpieces, self.num_components, self.num_inflation_eq,
            vtype=GRB.BINARY, name="inflate_assign"
        )

        # 摇匀阶段: 每个工件在摇匀设备上的开始/结束时间
        vars_dict['shake_start'] = self.model.addVars(
            self.num_workpieces, self.num_shake_eq,
            vtype=GRB.CONTINUOUS, name="shake_start"
        )
        vars_dict['shake_end'] = self.model.addVars(
            self.num_workpieces, self.num_shake_eq,
            vtype=GRB.CONTINUOUS, name="shake_end"
        )
        # 摇匀设备分配指示变量
        vars_dict['shake_assign'] = self.model.addVars(
            self.num_workpieces, self.num_shake_eq,
            vtype=GRB.BINARY, name="shake_assign"
        )

        # 分析阶段: 每个工件的每个组分在分析设备上的开始/结束时间
        vars_dict['analyze_start'] = self.model.addVars(
            self.num_workpieces, self.num_components, self.num_analysis_eq_per_comp,
            vtype=GRB.CONTINUOUS, name="analyze_start"
        )
        vars_dict['analyze_end'] = self.model.addVars(
            self.num_workpieces, self.num_components, self.num_analysis_eq_per_comp,
            vtype=GRB.CONTINUOUS, name="analyze_end"
        )
        # 分析设备分配指示变量
        vars_dict['analyze_assign'] = self.model.addVars(
            self.num_workpieces, self.num_components, self.num_analysis_eq_per_comp,
            vtype=GRB.BINARY, name="analyze_assign"
        )

        # 总完成时间
        vars_dict['makespan'] = self.model.addVar(vtype=GRB.CONTINUOUS, name="makespan")

        return vars_dict

    def _add_constraints(self):
        """添加约束条件"""
        v = self.vars

        # 1. 清洗阶段约束
        # 每个工件只能分配到一个清洗设备
        for wp in range(self.num_workpieces):
            self.model.addConstr(
                gp.quicksum(v['clean_assign'][wp, eq] for eq in range(self.num_clean_eq)) == 1,
                name=f"clean_assign_{wp}"
            )

        # 清洗时间约束
        for wp in range(self.num_workpieces):
            for eq in range(self.num_clean_eq):
                self.model.addConstr(
                    v['clean_end'][wp, eq] == v['clean_start'][wp, eq] + self.clean_time,
                    name=f"clean_time_{wp}_{eq}"
                )
                # 未分配的设备时间为0
                self.model.addConstr(
                    v['clean_start'][wp, eq] <= 1e6 * v['clean_assign'][wp, eq],
                    name=f"clean_start_zero_{wp}_{eq}"
                )

        # 清洗设备冲突约束 (同一设备上工件顺序约束)
        for eq in range(self.num_clean_eq):
            for wp1 in range(self.num_workpieces):
                for wp2 in range(self.num_workpieces):
                    if wp1 != wp2:
                        self.model.addConstr(
                            v['clean_end'][wp1, eq] <= v['clean_start'][wp2, eq] +
                            1e6 * (2 - v['clean_assign'][wp1, eq] - v['clean_assign'][wp2, eq]),
                            name=f"clean_conflict_{wp1}_{wp2}_{eq}"
                        )

        # 2. 充装阶段约束
        # 每个组分只能分配到一个充装设备
        for wp in range(self.num_workpieces):
            for comp in range(self.num_components):
                self.model.addConstr(
                    gp.quicksum(v['inflate_assign'][wp, comp, eq] for eq in range(self.num_inflation_eq)) == 1,
                    name=f"inflate_assign_{wp}_{comp}"
                )

        # 充装时间约束
        for wp in range(self.num_workpieces):
            for comp in range(self.num_components):
                for eq in range(self.num_inflation_eq):
                    self.model.addConstr(
                        v['inflate_end'][wp, comp, eq] == v['inflate_start'][wp, comp, eq] +
                        self.inflation_time[wp][comp],
                        name=f"inflate_time_{wp}_{comp}_{eq}"
                    )
                    self.model.addConstr(
                        v['inflate_start'][wp, comp, eq] <= 1e6 * v['inflate_assign'][wp, comp, eq],
                        name=f"inflate_start_zero_{wp}_{comp}_{eq}"
                    )

        # 充装设备冲突约束
        for eq in range(self.num_inflation_eq):
            for wp1 in range(self.num_workpieces):
                for comp1 in range(self.num_components):
                    for wp2 in range(self.num_workpieces):
                        for comp2 in range(self.num_components):
                            if (wp1 != wp2) or (comp1 != comp2):
                                self.model.addConstr(
                                    v['inflate_end'][wp1, comp1, eq] <= v['inflate_start'][wp2, comp2, eq] +
                                    1e6 * (2 - v['inflate_assign'][wp1, comp1, eq] - v['inflate_assign'][
                                        wp2, comp2, eq]),
                                    name=f"inflate_conflict_{wp1}_{comp1}_{wp2}_{comp2}_{eq}"
                                )

        # 充装必须在清洗完成后开始
        for wp in range(self.num_workpieces):
            for comp in range(self.num_components):
                clean_end = gp.quicksum(v['clean_end'][wp, eq] * v['clean_assign'][wp, eq]
                                        for eq in range(self.num_clean_eq))
                for eq in range(self.num_inflation_eq):
                    self.model.addConstr(
                        v['inflate_start'][wp, comp, eq] >= clean_end * v['inflate_assign'][wp, comp, eq],
                        name=f"inflate_after_clean_{wp}_{comp}_{eq}"
                    )

        # 3. 摇匀阶段约束
        # 每个工件只能分配到一个摇匀设备
        for wp in range(self.num_workpieces):
            self.model.addConstr(
                gp.quicksum(v['shake_assign'][wp, eq] for eq in range(self.num_shake_eq)) == 1,
                name=f"shake_assign_{wp}"
            )

        # 摇匀时间约束
        for wp in range(self.num_workpieces):
            for eq in range(self.num_shake_eq):
                self.model.addConstr(
                    v['shake_end'][wp, eq] == v['shake_start'][wp, eq] + self.shake_time[wp],
                    name=f"shake_time_{wp}_{eq}"
                )
                self.model.addConstr(
                    v['shake_start'][wp, eq] <= 1e6 * v['shake_assign'][wp, eq],
                    name=f"shake_start_zero_{wp}_{eq}"
                )

        # 摇匀设备冲突约束
        for eq in range(self.num_shake_eq):
            for wp1 in range(self.num_workpieces):
                for wp2 in range(self.num_workpieces):
                    if wp1 != wp2:
                        self.model.addConstr(
                            v['shake_end'][wp1, eq] <= v['shake_start'][wp2, eq] +
                            1e6 * (2 - v['shake_assign'][wp1, eq] - v['shake_assign'][wp2, eq]),
                            name=f"shake_conflict_{wp1}_{wp2}_{eq}"
                        )

        # 摇匀必须在所有组分充装完成后开始
        for wp in range(self.num_workpieces):
            inflate_end = gp.quicksum(
                v['inflate_end'][wp, comp, eq] * v['inflate_assign'][wp, comp, eq]
                for comp in range(self.num_components)
                for eq in range(self.num_inflation_eq)
            )
            for eq in range(self.num_shake_eq):
                self.model.addConstr(
                    v['shake_start'][wp, eq] >= inflate_end * v['shake_assign'][wp, eq],
                    name=f"shake_after_inflate_{wp}_{eq}"
                )

        # 4. 分析阶段约束
        # 每个组分只能分配到一个分析设备
        for wp in range(self.num_workpieces):
            for comp in range(self.num_components):
                self.model.addConstr(
                    gp.quicksum(v['analyze_assign'][wp, comp, eq]
                                for eq in range(self.num_analysis_eq_per_comp)) == 1,
                    name=f"analyze_assign_{wp}_{comp}"
                )

        # 分析时间约束
        for wp in range(self.num_workpieces):
            for comp in range(self.num_components):
                for eq in range(self.num_analysis_eq_per_comp):
                    self.model.addConstr(
                        v['analyze_end'][wp, comp, eq] == v['analyze_start'][wp, comp, eq] +
                        self.analysis_time[wp][comp],
                        name=f"analyze_time_{wp}_{comp}_{eq}"
                    )
                    self.model.addConstr(
                        v['analyze_start'][wp, comp, eq] <= 1e6 * v['analyze_assign'][wp, comp, eq],
                        name=f"analyze_start_zero_{wp}_{comp}_{eq}"
                    )

        # 分析设备冲突约束 (按组分区分设备)
        for comp in range(self.num_components):
            for eq in range(self.num_analysis_eq_per_comp):
                for wp1 in range(self.num_workpieces):
                    for wp2 in range(self.num_workpieces):
                        if wp1 != wp2:
                            self.model.addConstr(
                                v['analyze_end'][wp1, comp, eq] <= v['analyze_start'][wp2, comp, eq] +
                                1e6 * (2 - v['analyze_assign'][wp1, comp, eq] - v['analyze_assign'][wp2, comp, eq]),
                                name=f"analyze_conflict_{wp1}_{wp2}_{comp}_{eq}"
                            )

        # 分析必须在摇匀完成后开始
        for wp in range(self.num_workpieces):
            for comp in range(self.num_components):
                shake_end = gp.quicksum(v['shake_end'][wp, eq] * v['shake_assign'][wp, eq]
                                        for eq in range(self.num_shake_eq))
                for eq in range(self.num_analysis_eq_per_comp):
                    self.model.addConstr(
                        v['analyze_start'][wp, comp, eq] >= shake_end * v['analyze_assign'][wp, comp, eq],
                        name=f"analyze_after_shake_{wp}_{comp}_{eq}"
                    )

        # 5. 总完成时间约束
        # 总时间大于所有阶段的结束时间
        all_ends = []
        # 清洗阶段结束时间
        for wp in range(self.num_workpieces):
            all_ends.append(gp.quicksum(v['clean_end'][wp, eq] * v['clean_assign'][wp, eq]
                                        for eq in range(self.num_clean_eq)))
        # 充装阶段结束时间
        for wp in range(self.num_workpieces):
            for comp in range(self.num_components):
                all_ends.append(gp.quicksum(v['inflate_end'][wp, comp, eq] * v['inflate_assign'][wp, comp, eq]
                                            for eq in range(self.num_inflation_eq)))
        # 摇匀阶段结束时间
        for wp in range(self.num_workpieces):
            all_ends.append(gp.quicksum(v['shake_end'][wp, eq] * v['shake_assign'][wp, eq]
                                        for eq in range(self.num_shake_eq)))
        # 分析阶段结束时间
        for wp in range(self.num_workpieces):
            for comp in range(self.num_components):
                all_ends.append(gp.quicksum(v['analyze_end'][wp, comp, eq] * v['analyze_assign'][wp, comp, eq]
                                            for eq in range(self.num_analysis_eq_per_comp)))

        for end in all_ends:
            self.model.addConstr(v['makespan'] >= end, name=f"makespan_ge_{end}")

    def _set_objective(self):
        """设置目标函数: 最小化总完成时间"""
        self.model.setObjective(self.vars['makespan'], GRB.MINIMIZE)

    def solve(self, time_limit: int = 300):
        """求解模型"""
        self.model.setParam('TimeLimit', time_limit)  # 时间限制(秒)
        self.model.setParam('OutputFlag', 1)  # 显示求解过程
        self.model.optimize()

        if self.model.status == GRB.OPTIMAL:
            print(f"最优解总完成时间: {self.model.objVal:.2f}")
            self._print_schedule()
        elif self.model.status == GRB.TIME_LIMIT:
            print(f"达到时间限制，当前最佳解总完成时间: {self.model.objVal:.2f}")
            self._print_schedule()
        else:
            print(f"求解失败，状态码: {self.model.status}")

    def _print_schedule(self):
        """打印调度结果"""
        v = self.vars

        print("\n清洗阶段调度:")
        for wp in range(self.num_workpieces):
            for eq in range(self.num_clean_eq):
                if v['clean_assign'][wp, eq].x > 0.5:
                    print(
                        f"工件{wp} 在设备{eq} 开始: {v['clean_start'][wp, eq].x:.2f}, 结束: {v['clean_end'][wp, eq].x:.2f}")

        print("\n充装阶段调度:")
        for wp in range(self.num_workpieces):
            for comp in range(self.num_components):
                for eq in range(self.num_inflation_eq):
                    if v['inflate_assign'][wp, comp, eq].x > 0.5:
                        print(
                            f"工件{wp} 组分{comp} 在设备{eq} 开始: {v['inflate_start'][wp, comp, eq].x:.2f}, 结束: {v['inflate_end'][wp, comp, eq].x:.2f}")

        print("\n摇匀阶段调度:")
        for wp in range(self.num_workpieces):
            for eq in range(self.num_shake_eq):
                if v['shake_assign'][wp, eq].x > 0.5:
                    print(
                        f"工件{wp} 在设备{eq} 开始: {v['shake_start'][wp, eq].x:.2f}, 结束: {v['shake_end'][wp, eq].x:.2f}")

        print("\n分析阶段调度:")
        for wp in range(self.num_workpieces):
            for comp in range(self.num_components):
                for eq in range(self.num_analysis_eq_per_comp):
                    if v['analyze_assign'][wp, comp, eq].x > 0.5:
                        print(
                            f"工件{wp} 组分{comp} 在设备{eq} 开始: {v['analyze_start'][wp, comp, eq].x:.2f}, 结束: {v['analyze_end'][wp, comp, eq].x:.2f}")


if __name__ == "__main__":
    # 替换为你的算例文件路径
    instance_path = "gas_scheduling_instance.json"  # 用户可修改为自己的算例路径
    try:
        scheduler = GasSchedulingGurobi(instance_path)
        scheduler.solve(time_limit=300)  # 设置求解时间限制(秒)
    except Exception as e:
        print(f"错误: {str(e)}")